#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

#include "string_tools.h"
#include "steggen.h"
#include "steggen_valid.h"

#define BUFFER_SIZE 1024

size_t count_elements(char* array_line)
{
    char* eq_pos = strchr(array_line, '=');

    return chrn(eq_pos, '"') / 2;
}

steg_gen parse_steg_generator(char* filename)
{
    steg_gen s;

    char xbuffer[BUFFER_SIZE], ybuffer[BUFFER_SIZE];
    char *data;
    char *ptr;
    char **lines;
    size_t data_s = 0;
    size_t line_number = 0;

    size_t meta_lines = 0;
    size_t array_lines = 0;

    int *element_number;  // Number of elements in each array

    char **meta_var;
    char **meta_val;
    char **arr_name;
    char ***array;

    meta_error m_err = M_OK;
    array_error a_err = A_OK;

    register unsigned int i, j;     // General-purpose counter

    FILE * f;

    /* Reading the file line by line and removing all the whitespace outside
       quotation marks. Then adding it to the dynamically allocated *data
       which will become the whole data later. */

    f = fopen(filename, "r");

    if (f == NULL)
        exit(EXIT_FAILURE);

    data = malloc(0);   // Init for later realloc-usage in loop

    if (data == NULL)
        abort();

    while (fgets(xbuffer, BUFFER_SIZE, f) != NULL)
    {
        strtok(xbuffer, "\n");
        strtok(xbuffer, "#");
        memset(ybuffer, 0, BUFFER_SIZE);
        wwipe(ybuffer, xbuffer);
        if (ybuffer[0] != '#' && strlen(ybuffer) != 0)
        {
            data_s += (strlen(ybuffer) * sizeof(char));
            data = (char *)realloc(data, data_s);
            if (data == NULL)
                abort();
            strcat(data, ybuffer);
        }
    }

    fclose(f);

    line_number = chrn(data, ';');

    lines = malloc(line_number * sizeof(char*));
    if (lines == NULL)
        abort();

    for (i = 0; i < line_number; i++)
    {
        if (i == 0)        
            lines[0] = strtok(data, ";");
        else
            lines[i] = strtok(NULL, ";");
    }

    // Counts the number of lines with meta-declarations and arrays seperatly

    for (i = 0; i < line_number; i++)
    {
        if (lines[i][0] == '!')
            meta_lines++;
        if (lines[i][0] == '.')
            array_lines++;
    }

    // Is the number of meta and array declarations equal to the number of lines at all
    // (The opposite would mean an error!)

    if (meta_lines + array_lines != line_number)
    {
        printf("File corruption: A syntactical line that does neither start with '.' "
               "nor with '!' has been detected. Check for missing semicolons ';' and try again.\n");
        exit(EXIT_FAILURE);
    }

    // Are the meta and array lines ordered?

    for (i = 0; i < meta_lines; i++)
    {
        if (lines[i][0] != '!')
        {
            printf("File corruption: Non-standard order of meta and array declarations has been detected.\n");
            exit(EXIT_FAILURE);
        }
    }

    /* This example demonstrates the reason for ordering the lines:
       With the ordering, it is possible to read all meta lines or all array lines independently
       without special calculations, just using for-loops. */

    for (i = 0; i < meta_lines; i++)
    {
        m_err = validate_meta_line(lines[i]);
        if (m_err != M_OK)
        {
            printf("Error detected in line %u: %u\n", i + 1, m_err);
            // TODO Add text error description later
            exit(EXIT_FAILURE);
        }
    }

    for (i = meta_lines; i < line_number; i++)
    {
        a_err = validate_array_line(lines[i]);
        if (a_err != A_OK)
        {
            printf("Error detected in line %u: %u\n", i + 1, a_err);
            // TODO Add text error description later
            exit(EXIT_FAILURE);
        }
    }

    /* Sorting the meta and array lines, each for their own, makes it easier to search for
       duplicated variable naming: As soon as a variable string is equal to the previous,
       there is a duplication and the parsing has to be aborted. */

    qsort(lines, meta_lines, sizeof(char*), xstrcmp);
    qsort(&lines[meta_lines], array_lines, sizeof(char*), xstrcmp);

    for (i = 1; i < line_number; i++)
    {
        if (strchr(lines[i], '=') - lines[i] ==
            strchr(lines[i - 1], '=') - lines[i - 1])
            if (strncmp(lines[i], lines[i-1], strchr(lines[i], '=') - lines[i]) == 0)
            {
                printf("Duplicated line '%s' detected.\n", lines[i]);
                exit(EXIT_FAILURE);
            }
    }

    meta_var = malloc(meta_lines * sizeof(char*));
    meta_val = malloc(meta_lines * sizeof(char*));

    if (meta_var == NULL || meta_val == NULL)
        abort();

    for (i = 0; i < meta_lines; i++)
    {
        meta_var[i] = strtok(lines[i], "=");
        meta_val[i] = strtok(NULL, "=");

        meta_var[i]++;
        meta_val[i]++;
        strtok(meta_val[i], "\"");
    }

    element_number = malloc(array_lines * sizeof(int));
    if (element_number == NULL)
        abort();

    for (i = meta_lines; i < line_number; i++)
        element_number[i - meta_lines] = count_elements(lines[i]);

    arr_name = malloc(array_lines * sizeof(char*));
    array = malloc(array_lines * sizeof(char**));

    if (arr_name == NULL || array == NULL)
        abort();

    for (i = meta_lines; i < line_number; i++)
    {
        array[i - meta_lines] = malloc(element_number[i - meta_lines] * sizeof(char*));
        if (array[i - meta_lines] == NULL)
            abort();
    }

    for (i = meta_lines; i < line_number; i++)
    {
        arr_name[i - meta_lines] = strtok(lines[i], "=");
        arr_name[i - meta_lines]++;

        ptr = strtok(NULL, "=");
        // ptr++;
        for (j = 0; j < element_number[i - meta_lines]; j++)
        {
            if (j == 0)
                array[i - meta_lines][j] = strtok(ptr, ",");
            else
                array[i - meta_lines][j] = strtok(NULL, ",");

            array[i - meta_lines][j]++;
            strchr(array[i - meta_lines][j], '"')[0] = 0;
        }
    }

    s.data = data;
    s.meta_lines = meta_lines;
    s.array_lines = array_lines;

    s.element_number = element_number;

    s.meta_var = meta_var;
    s.meta_val = meta_val;
    s.arr_name = arr_name;
    s.array = array;

    free(lines);

    return s;
}

// Use atexit instead?
void free_steggen(steg_gen s)
{
    register unsigned int i;

    free(s.data);
    free(s.meta_val);
    free(s.meta_var);
    free(s.element_number);
    free(s.arr_name);

    for (i = s.meta_lines; i < s.array_lines + s.meta_lines; i++)
        free(s.array[i - s.meta_lines]);

    free(s.array);
}

float steggen_complexity(steg_gen s)
{
    float comp = 1;

    unsigned int i;

    for (i = 0; i < s.array_lines; i++)
        comp *= s.element_number[i];

    return log2(comp);
}

/* This won't work for arbitrary complex generators. The data type of v has yet
   to be replaced with the mpz_t data type from the GNU ??? TODO */

unsigned long long rev_prod(int *data, int n, int k)
{
    unsigned long long res = 1;

    for (0; k > 0; k--)
        res *= data[n-k];

    return res;
}

steggen_result call_steggen_value(steg_gen s, unsigned long long v)
{
    int *pos;
    unsigned int i;
    steggen_result sr;

    pos = malloc(s.array_lines * sizeof(int));

    for (i = 0; i < (s.array_lines - 1); i++)
        pos[i] = (v / rev_prod(s.element_number, s.array_lines, s.array_lines - (i + 1))) % s.element_number[i];

    pos[s.array_lines - 1] = v % s.element_number[s.array_lines - 1];

    sr.n = s.array_lines;
    sr.elems = pos;

    printf("<");

    for (i = 0; i < s.array_lines; i++)
        printf("%i,", pos[i]);

    printf("\b \b>\n");

    return sr;
}